package com.gluck.cases.boostrap.filter;

import com.auth0.jwt.exceptions.SignatureVerificationException;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.gluck.cases.boostrap.config.shiro.jwt.JwtToken;
import com.gluck.cases.common.utils.enums.BizSerErrorEnum;
import com.gluck.cases.common.utils.utils.AssertUtil;
import com.gluck.cases.common.utils.utils.JedisUtil;
import com.gluck.cases.common.utils.utils.JwtUtil;
import com.gluck.cases.common.utils.utils.PropertiesUtil;
import com.gluck.cases.core.modal.constants.AuthConstants;
import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
import org.apache.shiro.web.util.WebUtils;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletResponse;
import java.util.Objects;

/**
 * @ClassName JwtFilter
 * @Desc jwt 过滤器
 * @Author I am Gluck
 * @Date 2022/5/17 - 11:16 上午
 **/
public class JwtFilter extends BasicHttpAuthenticationFilter {

    /**
     * 这里我们详细说明下为什么最终返回的都是true，即允许访问
     * 例如我们提供一个地址 GET /article
     * 登入用户和游客看到的内容是不同的
     * 如果在这里返回了false，请求会被直接拦截，用户看不到任何东西
     * 所以我们在这里返回true，Controller中可以通过 subject.isAuthenticated() 来判断用户是否登入
     * 如果有些资源只有登入用户才能访问，我们只需要在方法上面加上 @RequiresAuthentication 注解即可
     * 但是这样做有一个缺点，就是不能够对GET,POST等请求进行分别过滤鉴权(因为我们重写了官方的方法)，但实际上对应用影响不大
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        // step 1 查看当前Header中是否携带Authorization属性(Token)，有的话就进行登录认证授权
        if (!this.isLoginAttempt(request, response)) {
            return false;
        }
        try {
            // step 2 进行Shiro的登录UserRealm
            this.executeLogin(request, response);
        } catch (Exception e) {
            // step 3 判断异常种类
            Throwable cause = e.getCause();
            // step 4 刷新过期token
            if (cause instanceof TokenExpiredException) {
                boolean refreshToken = isCanRefresh(request, response);
                AssertUtil.isFalse(refreshToken, BizSerErrorEnum.TOKEN_OVER_TIME, e.getMessage());
            }
            // step 5 SignatureVerificationException
            if (cause instanceof SignatureVerificationException) {
                AssertUtil.isTrue(true, BizSerErrorEnum.TOKEN_OR_SECRET_ERROR, e.getMessage());
            }
            // step 6 throw exception
            AssertUtil.isTrue(true, BizSerErrorEnum.TOKEN_FILTER_ERROR, e.getMessage());
        }
        return true;
    }

    /**
     * 功能描述: 防止重复调用executeLogin方法
     * @Params [request, response]
     * @Return boolean
     * @Author I`m Gluck
     * @Date 2022/5/18
     */
    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        this.sendChallenge(request, response);
        return true;
    }

    @Override
    protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
        String auth = this.getAuthzHeader(request);
        return auth != null;
    }

    @Override
    protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
        JwtToken jwtToken = new JwtToken(this.getAuthzHeader(request));
        this.getSubject(request, response).login(jwtToken);
        return true;
    }

    @Override
    protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
        return super.preHandle(request, response);
    }


    /**
     * 功能描述: 是否可以刷新token 不行则直接返回false
     * @Params [request, response]
     * @Return boolean
     * @Author I`m Gluck
     * @Date 2022/5/17
     */
    private boolean isCanRefresh(ServletRequest request, ServletResponse response) {
        // step 1 获取token
        String token = this.getAuthzHeader(request);
        // step 2 解密token 获取当前账号信息
        String username = JwtUtil.getClaim(token, AuthConstants.USERNAME);
        if(Objects.isNull(username)){
            return false;
        }
        // step 3 当前账号信息查询缓存RefreshToken是否存在
        if(JedisUtil.exists(AuthConstants.PREFIX_SHIRO_REFRESH_TOKEN + username)){
            return false;
        }
        // step 4 Redis中RefreshToken还存在，获取RefreshToken的时间戳
        String currentTimeMillisRedis = JedisUtil.getObject(AuthConstants.PREFIX_SHIRO_REFRESH_TOKEN + username).toString();
        // step 5 获取当前AccessToken中的时间戳，与RefreshToken的时间戳对比，如果当前时间戳一致，进行AccessToken刷新
        if(!currentTimeMillisRedis.equals(JwtUtil.getClaim(token, AuthConstants.CURRENT_TIME_MILLIS))){
            return false;
        }
        // step 6 执行刷新缓存逻辑
        refreshToken(request, response, username);
        return true;
    }

    /**
     * 功能描述: 刷新token
     * @Params [request, response]
     * @Return boolean
     * @Author I`m Gluck
     * @Date 2022/5/17
     */
    private void refreshToken(ServletRequest request, ServletResponse response, String username) {
        String refreshTime = String.valueOf(System.currentTimeMillis());
        PropertiesUtil.readProperties("config.properties");
        String refreshTokenExpireTime = PropertiesUtil.getProperty("refreshTokenExpireTime");
        // 设置缓存
        JedisUtil.setObject(AuthConstants.PREFIX_SHIRO_REFRESH_TOKEN + username, refreshTime, Integer.parseInt(refreshTokenExpireTime));
        String token = JwtUtil.sign(username, refreshTime);
        JwtToken jwtToken = new JwtToken(token);
        this.getSubject(request, response).login(jwtToken);
        // 最后将刷新的AccessToken存放在Response的Header中的Authorization字段返回
        HttpServletResponse httpServletResponse = WebUtils.toHttp(response);
        httpServletResponse.setHeader("Authorization", token);
        httpServletResponse.setHeader("Access-Control-Expose-Headers", "Authorization");
    }

}
